package com.atguigu.daijia.map.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.daijia.common.constant.RedisConstant;
import com.atguigu.daijia.common.constant.SystemConstant;
import com.atguigu.daijia.common.execption.GuiguException;
import com.atguigu.daijia.common.result.ResultCodeEnum;
import com.atguigu.daijia.common.util.LocationUtil;
import com.atguigu.daijia.driver.client.DriverInfoFeignClient;
import com.atguigu.daijia.map.repository.OrderServiceLocationRepository;
import com.atguigu.daijia.map.service.LocationService;
import com.atguigu.daijia.model.entity.driver.DriverSet;
import com.atguigu.daijia.model.entity.map.OrderServiceLocation;
import com.atguigu.daijia.model.form.map.OrderServiceLocationForm;
import com.atguigu.daijia.model.form.map.SearchNearByDriverForm;
import com.atguigu.daijia.model.form.map.UpdateDriverLocationForm;
import com.atguigu.daijia.model.form.map.UpdateOrderLocationForm;
import com.atguigu.daijia.model.vo.map.NearByDriverVo;
import com.atguigu.daijia.model.vo.map.OrderLocationVo;
import com.atguigu.daijia.model.vo.map.OrderServiceLastLocationVo;
import com.atguigu.daijia.order.client.OrderInfoFeignClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.*;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
@RequiredArgsConstructor
public class LocationServiceImpl implements LocationService {

    private final RedisTemplate redisTemplate;
    private final DriverInfoFeignClient driverInfoFeignClient;
    private final OrderServiceLocationRepository orderServiceLocationRepository;
    private final MongoTemplate mongoTemplate;
    private final OrderInfoFeignClient orderInfoFeignClient;

    /**
     * 司机开始接单，更新司机位置信息
     *
     * @param updateDriverLocationForm
     * @return
     */
    @Override
    public Boolean updateDriverLocation(UpdateDriverLocationForm updateDriverLocationForm) {

        Point point = new Point(updateDriverLocationForm.getLongitude().doubleValue(), updateDriverLocationForm.getLatitude().doubleValue());

        redisTemplate.opsForGeo().add(RedisConstant.DRIVER_GEO_LOCATION, point, updateDriverLocationForm.getDriverId().toString());

        return true;
    }

    /**
     * 司机停止接单，删除司机位置信息
     *
     * @param driverId
     * @return
     */
    @Override
    public Boolean removeDriverLocation(Long driverId) {
        redisTemplate.opsForGeo().remove(RedisConstant.DRIVER_GEO_LOCATION, driverId.toString());
        return true;
    }

    /**
     * 查询附近适合接单的司机
     *
     * @param searchNearByDriverForm
     * @return
     */
    @Override
    public List<NearByDriverVo> searchNearByDriver(SearchNearByDriverForm searchNearByDriverForm) {
        // 搜索下单乘客附近 5 公里内的司机
        // 定义搜索中心点
        Point point = new Point(searchNearByDriverForm.getLongitude().doubleValue(), searchNearByDriverForm.getLatitude().doubleValue());
        // 定义搜索半径
        Distance distance = new Distance(SystemConstant.NEARBY_DRIVER_RADIUS, RedisGeoCommands.DistanceUnit.KILOMETERS);
        // 定义搜索圈
        Circle circle = new Circle(point, distance);
        // 定义 Redis GeoRadius 查询参数对象 args
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                // 包含距离
                .includeDistance()
                // 包含坐标
                .includeCoordinates()
                // 排序：从近到远
                .sortAscending();

        GeoResults<RedisGeoCommands.GeoLocation<String>> result = redisTemplate.opsForGeo().radius(RedisConstant.DRIVER_GEO_LOCATION, circle, args);

        if (result == null) {
            throw new GuiguException(ResultCodeEnum.SEARCH_DRIVER_FAIL);
        }

        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = result.getContent();

        if (CollectionUtils.isEmpty(content)) {
            throw new GuiguException(ResultCodeEnum.DRIVER_NOT_FOUND);
        }

        List<NearByDriverVo> resultList = new ArrayList<>();
        for (GeoResult<RedisGeoCommands.GeoLocation<String>> item : content) {
            // 获取司机 ID
            long driverId = Long.parseLong(item.getContent().getName());
            // 获取司机乘客的当前距离
            BigDecimal currentDistance = BigDecimal.valueOf(item.getDistance().getValue()).setScale(2, RoundingMode.HALF_UP);
            // 获取司机设置信息
            DriverSet driverSet = driverInfoFeignClient.getDriverSet(driverId).getData();
            // 根据司机设置里的 OrderDistance，判断是否适合接单
            // OrderDistance 为 0 时，表示司机不设置订单里程，无论代驾订单距离多少，都愿意接单
            // 如果 OrderDistance 不为 0，只有 OrderDistance 大于等于代驾订单的里程，司机才愿意接单，否则不接单
            if (driverSet.getOrderDistance().doubleValue() != 0 && driverSet.getOrderDistance().subtract(searchNearByDriverForm.getMileageDistance()).doubleValue() < 0) {
                // continue 关键字跳过本次循环
                continue;
            }
            // 根据司机设置里的 AcceptDistance，判断是否适合接单
            // AcceptDistance 为 0 时，表示司机不设置接单距离，无论司机距离当前下单的代驾乘客多远，都愿意接单
            // 如果 AcceptDistance 不为 0，只有 AcceptDistance 大于等于司机乘客的当前距离，司机才愿意接单，否则不接单
            if (driverSet.getAcceptDistance().doubleValue() != 0 && driverSet.getAcceptDistance().subtract(currentDistance).doubleValue() < 0) {
                continue;
            }
            NearByDriverVo nearByDriverVo = new NearByDriverVo();
            nearByDriverVo.setDriverId(driverId);
            nearByDriverVo.setDistance(currentDistance);
            resultList.add(nearByDriverVo);
        }
        return resultList;
    }

    /**
     * 接单司机前往代驾订单起始点过程中更新司机位置信息到缓存
     * @param updateOrderLocationForm
     * @return
     */
    @Override
    public Boolean updateDriverLocationToCache(UpdateOrderLocationForm updateOrderLocationForm) {
        OrderLocationVo orderLocationVo = new OrderLocationVo();
        BeanUtils.copyProperties(updateOrderLocationForm, orderLocationVo);
        redisTemplate.opsForValue().set(RedisConstant.UPDATE_ORDER_LOCATION + updateOrderLocationForm.getOrderId(), JSON.toJSONString(orderLocationVo));
        return true;
    }

    /**
     * 从缓存中获取接单司机前往代驾订单起始点过程中的位置信息
     * @param orderId
     * @return
     */
    @Override
    public OrderLocationVo getDriverLocationFromCache(Long orderId) {
        String str = (String) redisTemplate.opsForValue().get(RedisConstant.UPDATE_ORDER_LOCATION + orderId);
        return JSON.parseObject(str, OrderLocationVo.class);
    }

    /**
     * 开始代驾服务后，批量保存订单服务过程中司机位置信息
     * @param orderServiceLocationFormList
     * @return
     */
    @Override
    public Boolean batchSaveLocationDuringService(List<OrderServiceLocationForm> orderServiceLocationFormList) {
        List<OrderServiceLocation> list = orderServiceLocationFormList.stream().map(item -> {
            OrderServiceLocation orderServiceLocation = new OrderServiceLocation();
            BeanUtils.copyProperties(item, orderServiceLocation);
            orderServiceLocation.setCreateTime(new Date());
            // 文档对应的 Java 实体类的主键类型是 String，因此这里需要转为 String 类型
            orderServiceLocation.setId(ObjectId.get().toString());
            return orderServiceLocation;
        }).collect(Collectors.toList());

        orderServiceLocationRepository.saveAll(list);

        return true;
    }

    /**
     * 开始代驾服务后，定时获取订单服务期间司机的最新位置信息
     * @param orderId
     * @return
     */
    @Override
    public OrderServiceLastLocationVo getOrderServiceLastLocation(Long orderId) {
        // 使用 MongoRepository 的 findAll 方法，查询所有符合条件的记录
        /*// 封装查询条件
        OrderServiceLocation orderServiceLocation = new OrderServiceLocation();
        orderServiceLocation.setOrderId(orderId);
        Example<OrderServiceLocation> example = Example.of(orderServiceLocation);
        // 按 createTime 降序排序
        Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
        List<OrderServiceLocation> list = orderServiceLocationRepository.findAll(example, sort);
        if (CollectionUtils.isEmpty(list)) {
            throw new GuiguException(ResultCodeEnum.DATA_ERROR);
        }
        // 获取集合中的第一个元素，就是订单服务期间最新位置信息
        OrderServiceLocation orderServiceLocationDb = list.get(0);
        OrderServiceLastLocationVo orderServiceLastLocationVo = new OrderServiceLastLocationVo();
        BeanUtils.copyProperties(orderServiceLocationDb, orderServiceLastLocationVo);
        return orderServiceLastLocationVo;*/

        // 使用 MongoTemplate 的 findOne 方法，查询符合条件的第一条记录
        Query query = new Query();
        query.addCriteria(Criteria.where("orderId").is(orderId));
        query.with(Sort.by(Sort.Direction.DESC, "createTime"));
        query.limit(1);
        OrderServiceLocation orderServiceLocationDb = mongoTemplate.findOne(query, OrderServiceLocation.class);
        if (orderServiceLocationDb == null) {
            throw new GuiguException(ResultCodeEnum.DATA_ERROR);
        }
        OrderServiceLastLocationVo orderServiceLastLocationVo = new OrderServiceLastLocationVo();
        BeanUtils.copyProperties(orderServiceLocationDb, orderServiceLastLocationVo);
        return orderServiceLastLocationVo;
    }

    /**
     * 计算订单实际距离
     * @param orderId
     * @return
     */
    @Override
    public BigDecimal calculateOrderRealDistance(Long orderId) {
        // 从 MongoDB 中获取订单服务期间司机的位置信息
        /*OrderServiceLocation orderServiceLocation = new OrderServiceLocation();
        orderServiceLocation.setOrderId(orderId);
        Example<OrderServiceLocation> example = Example.of(orderServiceLocation);
        List<OrderServiceLocation> orderServiceLocationList = orderServiceLocationRepository.findAll(example, Sort.by(Sort.Direction.ASC, "createTime"));*/

        /*
          MongoRepository支持通过方法命名规则来自定义查询方法。这种方法可以让你在不编写具体实现的情况下，通过方法名来定义复杂的查询逻辑。以下是自定义查询方法的一些基本规则和示例：
          1. 基本规则
                属性名称：方法名中的属性名称必须与实体类中的属性名称一致，都使用驼峰命名规则。
                关键字：方法名中可以包含一些关键字，如 find, get, count, delete 等。
                操作符：方法名中可以包含一些操作符，如 By, Where, And, Or 等。
          2. 常见关键字和操作符
                查询关键字：find, get, count, delete
                条件关键字：By, Where
                逻辑关键字：And, Or
                排序关键字：OrderBy, SortBy
                分页关键字：Pageable, Page
         */
        List<OrderServiceLocation> orderServiceLocationList = orderServiceLocationRepository.findByOrderIdOrderByCreateTimeAsc(orderId);

        // 计算订单实际距离
        double orderRealDistance = 0;
        if (!CollectionUtils.isEmpty(orderServiceLocationList)) {
            // 每次循环，计算两点之间的距离，并累加
            for (int i = 0; i < orderServiceLocationList.size() - 1; i++) {
                OrderServiceLocation startLocation = orderServiceLocationList.get(i);
                OrderServiceLocation endLocation = orderServiceLocationList.get(i + 1);
                // 计算两点之间的距离，并累加
                double distance = LocationUtil.getDistance(startLocation.getLatitude().doubleValue(), startLocation.getLongitude().doubleValue(), endLocation.getLatitude().doubleValue(), endLocation.getLongitude().doubleValue());
                orderRealDistance += distance;
            }
        }

        // 测试时，因为并没有真的发生代驾，因此这里模拟数据：实际代驾里程 = 预期里程 + 5
        if (orderRealDistance == 0) {
            return orderInfoFeignClient.getOrderInfo(orderId).getData().getExpectDistance().add(new BigDecimal(5));
        }
        return new BigDecimal(orderRealDistance);
    }
}
